Welcome to the Windows Land
Overview
Windows10下的堆管理主要分为两类:
Nt heap: 默认使用的内存管理机制
LowFragmentationHeap: win10下全新的内存管理机制
从使用角度可分为两类:
进程堆:存放在_PEB结构体中
私有堆: 通过HeapCreate返回的句柄来create
Back-End
_HEAP
1 | 0:004> dt _HEAP |
- +0x7c 8B: EncodeFlagMask: 设置为0x100000,表示需要encode该heap中chunk的header
- +0x80 16B: Encoding(_HEAP_ENTRY): 用来与chunk header xor的cookie
- +0x138 8B:BlocksIndex(_HEAP_LIST_LOOKUP): 指向 _HEAP_LIST_LOOKUP结构
- +0x150 8B: FreeList(_HEAP_ENTRY): 双向链表的head,类似于unsorted bin,但是是有序链表
- +0x198 8B: FrontEndHeap: 指向管理FrontEnd的Heap结构
- +0x1a8 8B: FrontEndHeapUsageData: 记录各种大小的chunk使用次数,到达一定次数就会启用Front-End
(header encoding: 在存放chunk header时,会将header ^ _HEAP->Encoding) 再存入)
_HEAP_ENTRY(chunk)
- Allocated chunk
- Freed chunk
- VitrualAlloc chunk
Allocated chunk
1 | 0:004> dt _HEAP_ENTRY |
- +0x00 8B: PreviousBlockPrivateData: 作为前一块heap的data,用于0x10对齐
- +0x08 2B: Size: 存放 size >> 4 的值
- +0x0a 1B: Flag: 0x1表示处于占用状态、0x2表示存在额外描述、0x4表示使用固定模式填充、0x8表示VirtualAlloc、0x10表示为该段最后一个chunk
- +0x0b 1B: SmallTagIndex: 是Size和Flag共三个字节的xor结果,用于检查header是否被修改
- +0x0c 2B: PreviousSize: 上一个chunk的size >> 4 的值
- +0x0e 1B: SegmentOffset: 某些情况下用来找segment
- +0x0f 1B: UnusedBytes: 在inuse的时候,表示
malloc
之后剩下的chunk的空间大小,可以用来判断chunk是来自于Front-End还是Back-End;在freed的时候,恒为0 - +0x10 : User Data区,在inuse的时候可以进行读写,在freed的时候存放Flink和Blink, Flink和Blink都指向User data区
_HEAP_VIRTUAL_ALLOC_ENTRY (VirtualAlloc chunk)
1 | 0:004> dt _HEAP_VIRTUAL_ALLOC_ENTRY |
- +0x00 16B: Entry: 链表的Flink和Blink,分别指向上一个和下一个通过VirtualAlloc分配出来的chunk。
- +0x30 8B: BusyBlock: 与普通的_HEAP_ENTRY头基本一样,不同在于这里的Size是没有使用的size,储存时也没有进行size >> 4的操作,UnusedBytes恒为4
_HEAP_LIST_LOOKUP
BlocksIndex指向的结构体,用于快速寻找合适的chunk
1 | 0:004> dt _HEAP_LIST_LOOKUP |
- +0x00 8B: ExtendedLookup: 指向下一个ExtendedLookup(通常管理更大chunk)
- +0x08 4B: ArraySize: 该结构管理的最大chunk的大小,通常为0x80(实际上是 0x800 >> 4 = 0x80)
- +0x10 4B: ItemCount: 目前该结构所管理的chunk数
- +0x14 4B: OutofRangeItems: 超出该结构管理大小的chunk数
- +0x18 4B: BaseIndex: 该结构所管理的chunk的起始index,用来从ListHint中找到合适大小的freed chunk
- +0x20 8B: ListHead: 指向Freelist的head
- +0x28 8B: ListsInUseUlong: bitmap,用来判断ListHint中是否有合适大小的chunk
- +0x30 8B: ListHints: 用来指向对应大小的chunk array,该array以0x10大小为间隔,存放一个对应size的freed chunk的地址
distribution mechanism
Allocate(RtlpAllocateHeap)
Size <= 0x4000
- 先去看该size对应的FrontEndHeapStatusBitmap是否启用LFH:
- 没有的话,会对对应的FrontEndHeapUsageData加上0x21
- 如果FrontEndHeapUsageData > 0xff00 || FrontEndHeapUsageData & 0x1f > 0x10,就会启用LFH
- 接下来会查看对应ListHint中是否有值(也就是是否有对应size的freed chunk):
- 如果有值,就检查该chunk的Flink是否是同样size的chunk:
- 若是则将Flink写入对应的Flink
- 若否则清空对应的Flink,并将该chunk从Freelist中unlink,并将header encoding
- 如果对应ListHint没有值,则从比较大的ListHint找:
- 如果找到了,就以上述同样方式处理该ListLink,并unlink找到的chunk,再切割该chunk为申请的大小,将剩下的chunk放入FreeList,对应大小的ListHint没值就会更新ListHint,最后将申请出来的chunk header encoding
- 如果没找到,就尝试ExtendHeap加大Heap空间,再从extend出来的chunk拿并切割,将剩下的chunk放入FreeList,对应大小的ListHint没值就会更新ListHint,最后将申请出来的chunk header encoding
- 如果有值,就检查该chunk的Flink是否是同样size的chunk:
- 最后放回分配chunk的指针
0x4000 < Size <= 0xff000
除了没有LFH相关的操作,其余都跟0x4000以下的情况一样
Size > 0xff000
直接调用ZwAllocateVirtualMemroy进行分配,类似于linux下的mmap直接给一大块地址,并且插入_HEAP->VirtualAllocdBlocks中
Free (RtlpFreeHeap)
Size <= 0xff000
- 先检查是否0x10对齐,利用unused byte判断该chunk状态(为0表示freed,反之就是inused)
- 如果是非LFH下,会对应的FrontEndHeapUsageData减1
- 然后判断前面的和后面的chunk是否freed,是的话就合并
- 此时会把前面的或后面的chunk从FreeList unlink,并更新ListHint
- 合并完后,update Size 和 PreviousSize,然后检查是不是最前和最后,是就插入,否则从ListHint中插入,并且Update ListHint,插入时会对FreeList检查(但是此检查不会触发abort,原因在于没有unlink写入)
Size > 0xff000
检查该chunk的linked list并从_HEAP->VirtualAllocdBlocks中移除,接着使用RtlpSecMemFreeVirtualMemory将chunk整个munmap掉
Back-End Exploitation
Unlink
基本上与Linux的Unlink类似,但要过更多的check:
- check sum(SmallTagIndex == Size & 0xff ^ Size >> 8 ^ Flag)
- double linked list的验证(chunk address->Flink->Blink == chunk address && chunk address->Blink->Flink == chunk address)
Exploit
- 利用unlink attack可实现将一个指针指向自身的效果
- 利用这个指针,可以控制周围的指针,达到任意地址读写的效果
- 因没有hook函数来控制程序流,所以只能通过leak stack address来打ROP或者shellcode
1 | binary base: |
- 然后就可以覆盖返回地址打ROP,调用VirtualProtect获得rwx权限,然后 jmp shellcode 或者 ROP执行system(“cmd.exe”)
Front-End
_LFH_HEAP
1 | 0:002> dt _LFH_HEAP |
- +0x18 8B: Heap: 指向对应的_HEAP
- +0x2a4 4B * 129: Buckets: 存放_HEAP_BUCKET结构体的数组,用来寻找配置大小对应到Block大小的阵列结构
- +0x4a8 8B * 129: SegmentInfoArrays: 存放
_HEAP_LOCAL_SEGMENT_INFO
结构体的数组,不同大小对应到不同的_HEAP_LOCAL_SEGMENT_INFO
结构体,主要管理对应到的_HEAP_SUBSEGMENT
的信息 - +0xcc0 0x28B: LocalData:
_HEAP_LOCAL_DATA
结构体,其中LocalData.LowFragHeap通常用于找回_LFH_HEAP
_HEAP_BUCKET
1 | 0:000> dt _HEAP_BUCKET |
- +0x0 2B: BlockUnits: 要分配出去的一个Block大小,存放的Size >> 4
- +0x2 1B: SizeIndex: 用户需要的大小 >> 4
_HEAP_LOCAL_SEGMENT_INFO
1 | ntdll!_HEAP_LOCAL_SEGMENT_INFO |
- +0x0 8B: LocalData: 指向对应
_HEAP_LOCAL_DATA
,即指向_LFH_HEAP->LocalData
- +0x8 8B: ActiveSubsegment:
_HEAP_SUBSEGMENT
的结构体指针,用于管理UserBlocks
,记录剩余等多chunk、该UserBlocks
最大分配数等信息 - +0x10 8 * 16B: CachedItems:
_HEAP_SUBSEGMENT
结构体指针数组,存放对应到该_HEAP_LOCAL_SEGMENT_INFO
且还有可以分配chunk给用户的_HEAP_SUBSEGMENT
指针,相当于内存池,当ActiveSubsegment
用完时,就从CachedItems
选择补充,替换掉ActiveSubsegment
- +0xac 8B: BucketIndex: 对应的
BucketIndex
,也就是_LFH_HEAP->Buckets
对应的下标
_HEAP_SUBSEGMENT
1 | ntdll!_HEAP_SUBSEGMENT |
- +0x0 8B: LocalInfo: 指回对应
_HEAP_LOCAL_SEGMENT_INFO
的结构体指针 - +0x8 8B: UserBlocks: 一个指向
_HEAP_USERDATA_HEADER
结构的指针,也就是指向LFH chunk的内存池 - +0x20 4B: AggregateExchg:
_INTERLOCK_SEQ
结构,储存对应的UserBlocks
的状态信息 - +0x24 2B: BlockSize: 该
UserBlocks
中每个chunk的大小 - +0x28 2B: BlockCount: 该
UserBlocks
中chunk的个数 - +0x2a 1B: SizeIndex: 该
UserBlocks
对应的index
_INTERLOCK_SEQ
1 | ntdll!_INTERLOCK_SEQ |
- +0x0 2B: Depth: 用来管理对应到的
UserBlocks
还有多少freed chunk,LFH会用这个判断是否还从该UserBlock进行分配 - +0x2 1b: Lock,即提供锁的作用,只占用第4 byte的最后一个bit
_HEAP_USERDATA_HEADER
1 | ntdll!_HEAP_USERDATA_HEADER |
- +0x0 8B: SubSegment: 指回对应的
_HEAP_SUBSEGMENT
结构 - +0x18 8B: EncodedOffsets:
_HEAP_USERDATA_OFFSETS
结构,用来验证chunk header是否被改过 - +0x20 0x10B: BusyBitmap: 记录该
UserBlocks
那些chunk被使用
_HEAP_ENTRY(LFH chunk)
1 | ntdll!_HEAP_ENTRY |
- +0x8 4B: SubSegmentCode: encode过的metadata,用来推回
UserBlocks
的位置 - +0xc 2B: PreviousSize: 该chunk在UserBlock中的index,实际上是第0xd个byte
- +0xf 1B: UnusedBytes: 用来判断该LFH chunk是否为freed状态,如果是busy状态,则为
0x80
Etc
_HEAP_USERDATA_HEADER->EncodedOffsets
在UserBlocks
初始化的时候设置,其算法为下面四个值进行xor:
- (sizeof(_HEAP_USERDATA_HEADER)) | ((_HEAP_BUCKET->BlockUnits) * 0x10 << 16)
- LFHkey
UserBlocks
的地址_LFH_HEAP
的地址
所有UserBlocks
里的chunk header在初始化的时候都会经过xor,其算法为下面各个值得xor:
_HEAP
的地址- LFHkey
- chunk本身的地址
address >> 4
- ((chunk address) - (UserBLocks address)) << 12
distribution mechanism
LFH initialization
在Back-End中也对LFH也有所提及,也就是在FrontEndHeapUsageData[x] & 0x1F > 0x10
的时候,置位_HEAP->CompatibilityFlag |= 0x20000000
,下一次Allocate
就会对LFH进行初始化
- 首先会ExtendFrontEndUsageData及增加更大的
_HEAP->BlocksIndex
,因为这里_HEAP->BlocksIndex
可以理解为一个_HEAP_LIST_LOOKUP
结构的单向链表(参考上面Back-End的解释),且默认初始情况下只存在一个管理比较小的(0x0 ~ 0x80)的chunk的_HEAP_LIST_LOOKUP
,所以这里会扩展到(0x80 ~ 0x400),即在链表尾追加一个管理更大chunk的_HEAP_LIST_LOOKUP
结构体结点 - 建立并初始化
_HEAP->FrontEndHeap
(通过mmap
),即初始化_LFH_HEAP
的一些metadata - 建立并初始化
_LFH_HEAP->SegmentInfoArrays[x]
,在SegmentInfoArrays[BucketIndex]
处填上对应的_HEAP_LOCAL_SEGMENT_INFO
结构体指针
再接下来Allocate
相同大小的chunk就会开始使用LFH:
- 分配
UserBlocks
并进行初始化,即设置对应大小的chunk - 然后再设置对应
_HEAP_LOCAL_SEGMENT_INFO->ActiveSubsegment
。 - 随机地从
UserBlocks
中返回一个chunk
Allocate (RtlpLowFragHeapAllocFromContext)
先看看
ActiveSubsegment
中有没有空闲的chunk,也就是通过ActiveSubsegment->AggregateExchg.depth
(free chunk的个数)判断:- 如果没有则从
CacheedItems
中找,找到有存在空闲chunk的Subsegment
就替换掉当前的ActiveSubsegment
- 如果有则继续往下
- 如果没有则从
取得
RtlpLowFragHeapRandomData[x]
上的值;且取值是依次循环取的,x为1 byte大小的值,即下一次x = (x + 1) % 256
;由于RtlpLowFragHeapRandomData
是一个存放256个随机数的数列(范围为0x0 ~ 0x7F
),所以这里相当于在取随机数计算相应的
UserBlocks
里chunk的index,通过RtlpLowFragHeapRandomData[x] * max_index >> 7
(其中max_index显然是能取到的最大的index):
- 如果发生了collision,即该index对应的chunk是busy的,那么往后取最近的;细节上,就是检查index对应到的bitmap是否为0,如果是0就返回对应的bitmap,否则选取最近的下一个
- 如果没有发生,则继续往下
检查
chunk->UnusedBytes & 0x3F != 0
,因为满足此式表示chunk是free状态的,否则状态非法;该过程中还会设置对应的bitmap,以及更新ActiveSubsegment->AggregateExchg.depth
等相关信息最后设置index(即
chunk->PreviousSize
)以及chunk->UnusedBytes
,并把chunk返回给用户
Free(RtlpFreeHeap)
- 首先更新
chunk->UnusedBytes
。 - 找到该chunk对应的在
UserBlocks
中的index,并且置UserBlocks->BusyBitmap
对应的bit为0 - 更新
ActiveSubsegment->AggregateExchg
- 如果该chunk不属于当前的
ActiveSubsegment
则看能不能放进CachedItems
中去,如果可以就放进去
Front-End Exploitation
Reused attack:
假如我们拥有UAF的漏洞可以利用,但是因为LFH分配的随机性,我们无法预测下一个那到的chunk是在哪个位置,也就是说现在我们free的chunk,下一次malloc不一定拿得到
那么此时可以通过填满UserBlocks
的方式,再free掉目标chunk,这样下一次malloc就必然会拿到目标chunk(因为只剩下一个),然后可以利用这个特性构造chunk overlap做进一步利用